/**
 * @typedef {{type: number, value, line: number, prev: Token}}
 */
var Token;

/**
 * @typedef {{tk: Token, line: number, compileJS:(function():string)}}
 */
var PToken;
	
(function(global){
	"use strict";
	
	var ms = function(){return +new Date()};
	
	/** @const */
	var DEBUG = false;
	
	var compOptions;
	/** @type {Array.<string>} */
	var extRequests;
	
	var avaliableExts = {
		'Object::extend':		'if(!Object.prototype.extend)Object.prototype.extend=function(a){for(var b in a)this[b]=a[b];return this};',
		'Math.clamp':			'if(!Math.clamp)Math.clamp=function(a,b,c){if(b>c)var d=c,c=b,b=d;a<b&&(a=b);a>c&&(a=c);return a};',
		'Math.truncate':		'if(!Math.truncate)Math.truncate=function(a){return ~~a};',
		'String::repeat':		'if(!String.prototype.repeat)String.prototype.repeat=function(b){for(var a="";b--;)a+=this;return a};',
		'String::trim':			'if(!String.prototype.trim)String.prototype.trim=function(){return this.replace(/^\\s+|\\s+$/g,"")};',
		'Function::bind':		'if(!Function.prototype.bind)Function.prototype.bind=function(d){function b(){return e.apply(this instanceof a?this:d||window,f.concat(c.call(arguments)))}function a(){}if(typeof this!=="function")throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var c=Array.prototype.slice,f=c.call(arguments,1),e=this;a.prototype=this.prototype;b.prototype=new a;return b};',
		'Array::indexOf':		'if(!Array.prototype.indexOf)Array.prototype.indexOf=function(d){if(this===void 0||this===null)throw new TypeError;var c=Object(this),b=c.length>>>0;if(b===0)return-1;var a=0;arguments.length>0&&(a=Number(arguments[1]),a!==a?a=0:a!==0&&a!==Infinity&&a!==-Infinity&&(a=(a>0||-1)*Math.floor(Math.abs(a))));if(a>=b)return-1;for(a=a>=0?a:Math.max(b-Math.abs(a),0);a<b;a++)if(a in c&&c[a]===d)return a;return-1};',
		'Array::lastIndexOf':	'if(!Array.prototype.lastIndexOf)Array.prototype.lastIndexOf=function(d){if(this===void 0||this===null)throw new TypeError;var c=Object(this),b=c.length>>>0;if(b===0)return-1;var a=b;arguments.length>1&&(a=Number(arguments[1]),a!==a?a=0:a!==0&&a!==1/0&&a!==-(1/0)&&(a=(a>0||-1)*Math.floor(Math.abs(a))));for(b=a>=0?Math.min(a,b-1):b-Math.abs(a);b>=0;b--)if(b in c&&c[b]===d)return b;return-1};',
		'Array::filter':		'if(!Array.prototype.filter)Array.prototype.filter=function(c,f){if(this===void 0||this===null)throw new TypeError;var b=Object(this),g=b.length>>>0;if(typeof c!=="function")throw new TypeError;for(var d=[],a=0;a<g;a++)if(a in b){var e=b[a];c.call(f,e,a,b)&&d.push(e)}return d};',
		'Array::forEach':		'if(!Array.prototype.forEach)Array.prototype.forEach=function(c,d){var e,a;if(this==null)throw new TypeError(" this is null or not defined");var b=Object(this),g=b.length>>>0;if({}.toString.call(c)!="[object Function]")throw new TypeError(c+" is not a function");d&&(e=d);for(a=0;a<g;){var f;a in b&&(f=b[a],c.call(e,f,a,b));a++}};',
		'Array::every':			'if(!Array.prototype.every)Array.prototype.every=function(c,d){if(this===void 0||this===null)throw new TypeError;var b=Object(this),e=b.length>>>0;if(typeof c!=="function")throw new TypeError;for(var a=0;a<e;a++)if(a in b&&!c.call(d,b[a],a,b))return false;return true};',
		'Array::map':			'if(!Array.prototype.map)Array.prototype.map=function(d,f){var g,e,a;if(this==null)throw new TypeError(" this is null or not defined");var b=Object(this),h=b.length>>>0;if({}.toString.call(d)!="[object Function]")throw new TypeError(d+" is not a function");f&&(g=f);e=Array(h);for(a=0;a<h;){var c;a in b&&(c=b[a],c=d.call(g,c,a,b),e[a]=c);a++}return e};',
		'Array::some':			'if(!Array.prototype.some)Array.prototype.some=function(c,d){if(this===void 0||this===null)throw new TypeError;var b=Object(this),e=b.length>>>0;if(typeof c!=="function")throw new TypeError;for(var a=0;a<e;a++)if(a in b&&c.call(d,b[a],a,b))return true;return false};',
		'Array::reduce':		'if(!Array.prototype.reduce)Array.prototype.reduce=function(d){var a,c=this.length,b;if(typeof d!=="function")throw new TypeError("First argument is not callable");if((c==0||c===null)&&arguments.length<=1)throw new TypeError("Array length is 0 and no second argument");arguments.length<=1?(b=this[0],a=1):b=arguments[1];for(a=a||0;a<c;++a)a in this&&(b=d.call(void 0,b,this[a],a,this));return b};',
		'Array::reduceRight':	'if(!Array.prototype.reduceRight)Array.prototype.reduceRight=function(d){if(this===void 0||this===null)throw new TypeError;var c=Object(this),a=c.length>>>0;if(typeof d!=="function")throw new TypeError;if(a===0&&arguments.length===1)throw new TypeError;a-=1;var b;if(arguments.length>=2)b=arguments[1];else{do{if(a in this){b=this[a--];break}if(--a<0)throw new TypeError;}while(1)}for(;a>=0;)a in c&&(b=d.call(void 0,b,c[a],a,c)),a--;return b};',
		'Array::last':			'if(!Array.prototype.last)Array.prototype.last=function(){if(!this.length)return void 0;return this[this.length-1]};',
		'Array::iterator':		'Array.prototype.iterator=function(){var a=this.concat(),b=0,c={hasNext:function(){return b<a.length},next:function(){return a[b++]},iterator:function(){return c}};return c};',
		'Array.isArray':		'if(!Array.isArray)Array.isArray=function(a){return a instanceof Array};',
		'Date::toISOString':	'if(!Date.prototype.toISOString||(new Date(-621987552E5)).toISOString().indexOf("-000001")===-1)Date.prototype.toISOString=function(){var b,c,d,a;if(!isFinite(this))throw new RangeError;b=[this.getUTCMonth()+1,this.getUTCDate(),this.getUTCHours(),this.getUTCMinutes(),this.getUTCSeconds()];a=this.getUTCFullYear();a=(a<0?"-":a>9999?"+":"")+("00000"+Math.abs(a)).slice(0<=a&&a<=9999?-4:-6);for(c=b.length;c--;)d=b[c],d<10&&(b[c]="0"+d);return a+"-"+b.slice(0,2).join("-")+"T"+b.slice(2).join(":")+"."+("000"+this.getUTCMilliseconds()).slice(-3)+"Z"};',
		'Date.now':				'if(!Date.now)Date.now=function(){return+new Date};',
		'range':				'var range=(function(){function c(a,b,d){if(!(this instanceof c))return new c(a,b,d);a=~~a;b=~~b;if(a>b)var e=a,a=b,b=e;this._min=a;this._max=b;this._step=d||1}c.prototype.iterator=function(){var a=this._min,b=this._max,d=this._step,c={hasNext:function(){return a<b},next:function(){return a+=d},iterator:function(){return c}};return c};return(function(a,b,d){return new c(a,b,d)})})();',
		'':''
	};
	
	/**
		@param {...*} var_args
	*/
	function log(var_args){
		var arr = [];
		arr.push('JSE: ');
		for(var i=0;i<arguments.length;++i) arr.push(arguments[i]);
		console.log.apply(console, arr);
	};
	
	/**
	 * @enum {number}
	 * @const
	 */
	var tokensType = {
		EOF:			0,
		STRING:			1,
		NUMBER:			2,
		COLON:			3,
		SEMICOLON:		4,
		DOT:			5,
		COMMA:			6,
		CURLY_OPEN:		7,
		CURLY_CLOSE:	8,
		ROUND_OPEN:		9,
		ROUND_CLOSE:	10,
		SQUARE_OPEN:	11,
		SQUARE_CLOSE:	12,
		CONST:			13,
		OPERATOR:		14,
		NEW:			15,
		VAR:			16,
		IDENTIFIER:		17,
		INCREMENT:		18,
		DECREMENT:		19,
		FUNCTION:		20,
		REGEX:			21,
		ASSIGN:			22,
		RETURN:			23,
		FOR:			24,
		WHILE:			25,
		IF:				26,
		ELSE:			27,
		DO:				28,
		DELAY:			29,
		SWITCH:			30,
		SCOPE:			31,
		EACH:			32,
		AT:				33,
		CASE:			34,
		DEFAULT:		35,
		NULL:			36,
		TRUE:			37,
		FALSE:			38,
		UNDEFINED:		39,
		ARROW:			40,
		THROW:			41,
		IN:				42,
		ELLIPSIS:		43,
		CONTINUE:		44,
		BREAK:			45,
		IS:				46,
		DCOLON:			47,
		QUESTIONMARK:	48
	};
	
	var curTmpVarNum = 0;
	function getTmpVarName(){
		function randString(len){
			var str = '';
			var chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
			for(var i=0;i<len;++i){
				str += chars[Math.floor(chars.length * Math.random())];
			}
			return str;
		}
		return '$JSETMP$'+curTmpVarNum+'$'+randString(8);
	}
	
	
	/**
	 * Compiles the code to javascript
	 * 
	 * @param {string} code Code to compile
	 */
	function compile(code){
		compOptions = {};
		extRequests = [];
		
		var started = ms();
		var tokens = tokenize(code);
		var tokenizeFinished = ms();
		
		var parsed = parse(tokens);
		var parserFinished = ms();
		
		var compiled = parsed.compileJS();
		var compilerFinished = ms();
		
		if(DEBUG){
			log('Tokenize time: ', tokenizeFinished - started);
			log('Parser time: ', parserFinished - tokenizeFinished);
			log('Compile time: ', compilerFinished - parserFinished);
			log('Total time: ', compilerFinished - started);
		}
		
		compiled = '(function(){' + compiled + '})();';
		
		var i = extRequests.length;
		while(i--){
			compiled = avaliableExts[extRequests[i]]+';' + compiled;
		}
		
		return compiled;
	}
	
	/**
	 * Tokenizes the code
	 * 
	 * @param {string} code Code to tokenize
	 * @return {Array.<Token>} Token array
	 */
	function tokenize(code){
		/**
		 * @type {Array.<Token>}
		*/
		var result = [];
		
		var newline = /^(\r\n|\n|\r)/;
		var curLine = 1;
		var lastToken = null;
		
		/**
		 * Pushes a token to the result
		 *
		 * @param {number} type
		 * @param {*=} value
		 */
		function pushToken(type, value){
			var tk = {type:type, value:value != null ? value : null, line:curLine, prev:lastToken};
			result.push(tk);
			lastToken = tk;
		}
		
		var i = 0;
		
		/**
			@type {Object.<string, string>}
		*/
		var definitions = {};
		
		// Ignore shebang
		if(code.charAt(0) == '#' && code.charAt(1) == '!'){
			var nextLine = code.indexOf('\n');
			if(nextLine != -1){
				i = nextLine;
				curLine = 2;
			}
		}
		
		
		for(;i<code.length;++i){
			var tmpStr = code.slice(i);
			var ch = tmpStr.charAt(0);
			var match;
			
			if((/^\r\n/).test(tmpStr)){
				++curLine;
				//pushToken(tokensType.NEWLINE);
				
				++i; // Skip the \n
			}else if(ch == '\r' || ch == '\n'){
				++curLine;
				//pushToken(tokensType.NEWLINE);
				
			}else if((/^\s/).test(tmpStr)){
				i += tmpStr.match(/^[ \t\v\f]+/)[0].length - 1;
				
			}else if(ch == '#'){
				var cmd = tmpStr.slice(1).split(/\s/, 1)[0];
				
				
				switch(cmd){
					case 'define':
						match = tmpStr.match(/#define\s+([a-zA-Z0-9_$]+)\s*([a-zA-Z0-9_$]*)[\r\n]/);
						if(match == null) throw new Error("Invalid definition at line "+curLine);
						definitions[match[1]] = match[2];
						i += match[0].length - 1;
						--i; // Reparse the newline
					break;
					
					case 'undefine':
						match = tmpStr.match(/#undefine\s+([a-zA-Z0-9_$]+)[\r\n]/);
						if(match == null) throw new Error("Invalid undefinition at line "+curLine);
						delete definitions[match[1]];
						i += match[0].length - 1;
						--i; // Reparse the newline
					break;
					
					case 'use':
						match = tmpStr.match(/#use\s+([a-zA-Z0-9_$.:\[\]]+)[\r\n]/);
						if(match == null) throw new Error("Invalid extension request at line "+curLine);
						
						if(!avaliableExts.hasOwnProperty(match[1])){
							throw new Error("Couldn't find extension '"+match[1]+"' at line "+curLine);
						}
						
						if(extRequests.indexOf(match[1]) == -1) extRequests.push(match[1]);
						
						i += match[0].length - 1;
						--i; // Reparse the newline
					break;
					
					case 'include':
					
					break;
					
					//TODO: These commands
					/*
					case 'if_defined':
						
					break;
					case 'if_not_defined':
						
					break;
					case 'else':
					
					break;
					case 'endif':
					
					break;
					*/
					
					default: throw new Error("Unknown preprocessor command \""+cmd+"\" at line "+curLine);
				}
				
			}else if(match = tmpStr.match(/^\/\/[^\r\n]*/)){
				i += match[0].length - 1;
				
			}else if(match = tmpStr.match(/^\/\*([\s\S]*?)\*\//)){
				i += match[0].length - 1;
				
				var tmp = match[0].match(/(\r\n|\n|\r)/g);
				curLine += tmp ? tmp.length : 0;
				
			}else if(match = tmpStr.match(/^0[xX][0-9a-fA-F]+/)){
				pushToken(tokensType.NUMBER, parseInt(match[0], 16));
				i += match[0].length - 1;
				
			}else if(match = tmpStr.match(/^[0-9]+(?:\.[0-9]+)?/)){
				pushToken(tokensType.NUMBER, parseFloat(match[0]));
				i += match[0].length - 1;
				
			}else if(match = tmpStr.match(/^~\/(\\.|[^\/])+\/[gim]*/)){
				pushToken(tokensType.REGEX, match[0].slice(1));
				i += match[0].length - 1;
				
			}else if((/^@"/).test(tmpStr)){
				match = tmpStr.match(/^@"(\\u[0-9a-fA-F]{4}|\\x[0-9a-fA-F]{2}|\\[a-zA-Z"']|[^"\r\n])*"/);
				if(match == null) throw new Error("Invalid string at line "+curLine);
				pushToken(tokensType.STRING, match[0].slice(1).replace(/\\/gm, "\\\\"));
				i += match[0].length - 1;
				
			}else if((/^@'/).test(tmpStr)){
				match = tmpStr.match(/^@'(\\u[0-9a-fA-F]{4}|\\x[0-9a-fA-F]{2}|\\[a-zA-Z"']|[^'\r\n])*'/);
				if(match == null) throw new Error("Invalid string at line "+curLine);
				pushToken(tokensType.STRING, match[0].slice(1).replace(/\\/gm, "\\\\"));
				i += match[0].length - 1;
				
			}else if(ch == '"'){
				match = tmpStr.match(/^"(\\u[0-9a-fA-F]{4}|\\x[0-9a-fA-F]{2}|\\[a-zA-Z"']|[^"\r\n])*"/);
				if(match == null) throw new Error("Invalid string at line "+curLine);
				pushToken(tokensType.STRING, match[0]);
				i += match[0].length - 1;
				
			}else if(ch == "'"){
				match = tmpStr.match(/^'(\\u[0-9a-fA-F]{4}|\\x[0-9a-fA-F]{2}|\\[a-zA-Z"']|[^'\r\n])*'/);
				if(match == null) throw new Error("Invalid string at line "+curLine);
				pushToken(tokensType.STRING, match[0]);
				i += match[0].length - 1;
				
			}else if(match = tmpStr.match(/^\.\.\./)){
				pushToken(tokensType.ELLIPSIS);
				i += 2;
			}else if(ch == '('){
				pushToken(tokensType.ROUND_OPEN);
			}else if(ch == ')'){
				pushToken(tokensType.ROUND_CLOSE);
			}else if(ch == '{'){
				pushToken(tokensType.CURLY_OPEN);
			}else if(ch == '}'){
				pushToken(tokensType.CURLY_CLOSE);
			}else if(ch == '['){
				pushToken(tokensType.SQUARE_OPEN);
			}else if(ch == ']'){
				pushToken(tokensType.SQUARE_CLOSE);
			}else if(ch == '.'){
				pushToken(tokensType.DOT);
			}else if(ch == ','){
				pushToken(tokensType.COMMA);
			}else if((/^::/).test(tmpStr)){
				pushToken(tokensType.DCOLON);
				++i;
			}else if(ch == ':'){
				pushToken(tokensType.COLON);
			}else if(ch == ';'){
				pushToken(tokensType.SEMICOLON);
			}else if((/^=>/).test(tmpStr)){
				pushToken(tokensType.ARROW);
				++i;
			}else if(ch == '@'){
				pushToken(tokensType.AT);
			}else if(ch == '?'){
				pushToken(tokensType.QUESTIONMARK);
			
			}else if((/^(&&|\|\|)/).test(tmpStr)){
				pushToken(tokensType.OPERATOR, ch + ch);
				++i;
			}else if((/^[=!><]=/).test(tmpStr)){
				switch(ch){
					case '=':
						pushToken(tokensType.OPERATOR, '===');
					break;
					case '!':
						pushToken(tokensType.OPERATOR, '!==');
					break;
					default: pushToken(tokensType.OPERATOR, ch + '=');;
				}
				
				++i;
				
			}else if(match = tmpStr.match(/^([-+*~^&|%\/]|>>|<<|>>>)?=/)){
				pushToken(tokensType.ASSIGN, match[0]);
				i += match[0].length - 1;
			
			}else if((/^\+\+/).test(tmpStr)){
				pushToken(tokensType.INCREMENT);
				++i;
			}else if((/^\-\-/).test(tmpStr)){
				pushToken(tokensType.DECREMENT);
				++i;
			}else if((/^(>>|<<)/).test(tmpStr)){
				pushToken(tokensType.OPERATOR, tmpStr.slice(0, 2));
				++i;
			}else if((/^<<</).test(tmpStr)){
				pushToken(tokensType.OPERATOR, '<<<');
				i+=2;
			}else if((/^[<>]/).test(tmpStr)){
				pushToken(tokensType.OPERATOR, ch);
			}else if((/^[-+*~^&|%\/!]/).test(tmpStr)){
				pushToken(tokensType.OPERATOR, ch);
			
			}else if(match = tmpStr.match(/^[a-zA-Z0-9_$]+/)){
				var value = match[0];
				
				
				switch(value){
					case 'new': pushToken(tokensType.NEW); break;
					case 'var': pushToken(tokensType.VAR); break;
					case 'const': pushToken(tokensType.CONST); break;
					case 'function': pushToken(tokensType.FUNCTION); break;
					case 'return': pushToken(tokensType.RETURN); break;
					case 'if': pushToken(tokensType.IF); break;
					case 'else': pushToken(tokensType.ELSE); break;
					case 'while': pushToken(tokensType.WHILE); break;
					case 'do': pushToken(tokensType.DO); break;
					case 'for': pushToken(tokensType.FOR); break;
					case 'delay': pushToken(tokensType.DELAY); break;
					case 'switch': pushToken(tokensType.SWITCH); break;
					case 'scope': pushToken(tokensType.SCOPE); break;
					case 'each': pushToken(tokensType.EACH); break;
					case 'case': pushToken(tokensType.CASE); break;
					case 'default': pushToken(tokensType.DEFAULT); break;
					//case 'has': pushToken(tokensType.HAS); break;
					case 'null': pushToken(tokensType.NULL); break;
					case 'true': pushToken(tokensType.TRUE); break;
					case 'false': pushToken(tokensType.FALSE); break;
					case 'undefined': pushToken(tokensType.UNDEFINED); break;
					case 'throw': pushToken(tokensType.THROW); break;
					case 'in': pushToken(tokensType.IN); break;
					case 'continue': pushToken(tokensType.CONTINUE); break;
					case 'break': pushToken(tokensType.BREAK); break;
					case 'is': pushToken(tokensType.IS); break;
					
					default:
						if(definitions.hasOwnProperty(value)){
							pushToken(tokensType.IDENTIFIER, definitions[value]);
						}else{
							pushToken(tokensType.IDENTIFIER, value);
						}
					break;
				}
				i += match[0].length - 1;
			}else{
				throw new Error("Invalid token '"+tmpStr.slice(0, 10)+"...' at line "+curLine);
			}
		}
		
		return result;
	}
	
	function getTokenName(tk){
		switch(tk.type){
			case tokensType.EOF: return 'EOF';
			case tokensType.STRING: return 'STRING('+tk.value+')';
			case tokensType.NUMBER: return 'NUMBER('+tk.value+')';
			case tokensType.COLON: return 'COLON';
			case tokensType.SEMICOLON: return 'SEMICOLON';
			case tokensType.DOT: return 'DOT';
			case tokensType.COMMA: return 'COMMA';
			case tokensType.CURLY_OPEN: return 'CURLY_OPEN';
			case tokensType.CURLY_CLOSE: return 'CURLY_CLOSE';
			case tokensType.ROUND_OPEN: return 'ROUND_OPEN';
			case tokensType.ROUND_CLOSE: return 'ROUND_CLOSE';
			case tokensType.SQUARE_OPEN: return 'SQUARE_OPEN';
			case tokensType.SQUARE_CLOSE: return 'SQUARE_CLOSE';
			case tokensType.CONST: return 'CONST';
			case tokensType.OPERATOR: return 'OPERATOR('+tk.value+')';
			case tokensType.NEW: return 'NEW';
			case tokensType.VAR: return 'VAR';
			case tokensType.IDENTIFIER: return 'IDENTIFIER('+tk.value+')';
			case tokensType.INCREMENT: return 'INCREMENT';
			case tokensType.DECREMENT: return 'DECREMENT';
			case tokensType.FUNCTION: return 'FUNCTION'
			case tokensType.REGEX: return 'REGEX';
			case tokensType.ASSIGN: return 'ASSIGN';
			case tokensType.RETURN: return 'RETURN';
			case tokensType.FOR: return 'FOR';
			case tokensType.WHILE: return 'WHILE';
			case tokensType.IF: return 'IF';
			case tokensType.ELSE: return 'ELSE';
			case tokensType.DO: return 'DO';
			case tokensType.DELAY: return 'DELAY';
			case tokensType.SWITCH: return 'SWITCH';
			case tokensType.SCOPE: return 'SCOPE';
			case tokensType.EACH: return 'EACH';
			case tokensType.AT: return 'AT';
			case tokensType.CASE: return 'CASE';
			case tokensType.DEFAULT: return 'DEFAULT';
			case tokensType.ARROW: return 'ARROW';
			case tokensType.THROW: return 'THROW';
			case tokensType.IN: return 'IN';
			case tokensType.ELLIPSIS: return 'ELLIPSIS';
			case tokensType.CONTINUE: return 'CONTINUE';
			case tokensType.BREAK: return 'BREAK';
			case tokensType.IS: return 'IS';
			case tokensType.DCOLON: return 'DCOLON';
			case tokensType.QUESTIONMARK: return 'QUESTIONMARK';
		}
		return 'UNKNOWN('+tk.type+')';
	}
	
	/**
	 * Parses an array of tokens
	 * 
	 * @param {Array.<Token>} tokens Token array
	 */
	function parse(tokens){
		var curTokenNum = -1;
		var curParserLine = 1;
		
		var parser = {
			EOF: {type: tokensType.EOF, value:null, line: (tokens.length > 0 ? tokens[tokens.length-1].line : 1)},
			nextToken: function(){
				return tokens[++curTokenNum] || parser.EOF;
			},
			
			back: function(n){
				curTokenNum -= n;
			},
			
			skip: function(n){
				curTokenNum += n;
			},
			
			getToken: function(n){
				return tokens[n||0] || parser.EOF;
			},
			
			peek: function(n){
				return tokens[curTokenNum+(n==null?1:n)] || parser.EOF;
			},
			
			assertToken: function(n, types, fatal){
				var tk = parser.getToken(n + curTokenNum);
				if(types.indexOf(tk.type) === -1){
					if(fatal) parser.unexpected(tk, types);
					return false;
				}
				return true;
			},
			
			unexpected: function(tk, expecting){
				if(expecting) throw new Error("Unexpected " + getTokenName(tk) + " (expecting "+expecting+") at line " + tk.line + " (curTokenNum = "+curTokenNum+")");
				
				throw new Error("Unexpected " + getTokenName(tk) + " at line " + tk.line + " (curTokenNum = "+curTokenNum+")");
			}
		};
		
		
		/**
		 * Creates a PToken
		 * 
		 * @param {Token} tk
		 * @param {(function():string)} compileJS
		 * @param {Object=} obj
		 * @return {PToken}
		 */
		function createPToken(tk, compileJS, obj){
			/** @type {PToken} */
			var tmp = {tk:tk, line:tk.line, compileJS: function(){
				var nl = '';
				var i = Math.max(tk.line - curParserLine, 0);
				if(i > 0) curParserLine = tk.line;
				while(i--) nl += '\n';
				
				return nl + compileJS.call(this);
			}};
			if(obj) for(var i in obj) if(obj.hasOwnProperty(i)) tmp[i] = obj[i];
			return tmp;
		}
		
		function makeStatement(){
			var arr = [];
			var tk = parser.getToken();
			
			return arr;
		}
		
		function generateLeftOperatorParser(higherPrecedence, types){
			return function(){
				var left = higherPrecedence();
				var tk = parser.nextToken();
				while(tk.type == tokensType.OPERATOR && (types.indexOf(tk.value) != -1)){
					var ptk = createPToken(tk, function(){
						return '('+this.left.compileJS() + this.op + this.right.compileJS()+')';
					}, {
						left: left,
						op: tk.value,
						right: higherPrecedence()
					});
					
					left = ptk;
					tk = parser.nextToken();
				}
				
				parser.back(1);
				
				return left;
			};
		}
		
		function primaryExpression(){
			var tk = parser.nextToken();
			if(tk.type == tokensType.NUMBER){
				return createPToken(tk, function(){return '('+tk.value+')'});
			}else if(tk.type == tokensType.STRING){
				return createPToken(tk, function(){return '('+tk.value+')'});
			}else if(tk.type == tokensType.AT){
				parser.back(1);
				return getLeftHandAssignment();
			}else if(tk.type == tokensType.IDENTIFIER){
				parser.back(1);
				return getLeftHandAssignment();
			//	return createPToken(tk, function(){return '('+tk.value+')'});
			}else if(tk.type == tokensType.OPERATOR && (tk.value == '+' || tk.value == '-' || tk.value == '!' || tk.value == '~' || tk.value == '-')){
				return createPToken(tk, function(){
					return '('+this.op+this.exp.compileJS()+')';
				},
				{
					op: tk.value,
					exp: primaryExpression()
				});
			}else if(tk.type == tokensType.ROUND_OPEN){
				var exp = commaExpression();
				var next = parser.nextToken();
				if(next.type != tokensType.ROUND_CLOSE){
					if(tk.line != next.line){
						throw new Error("Mismatching round brackets at lines " + tk.line + " and "+next.line);
					}else{
						throw new Error("Mismatching round brackets at line " + tk.line + " (found "+getTokenName(next)+")");
					}
				}
				
				return createPToken(tk, function(){
					return '(' + exp.compileJS() + ')';
				});
			}else if(tk.type == tokensType.SQUARE_OPEN){
				var arrValues = [];
				
				var next = parser.peek(1);
				if(next.type != tokensType.SQUARE_CLOSE){
					while(true){
						var exp = expression();
						arrValues.push(exp);
						
						next = parser.peek(1);
						if(next.type == tokensType.SQUARE_CLOSE){
							break;
						}else if(next.type == tokensType.COMMA){
							parser.skip(1);
							if(parser.peek(1).type == tokensType.SQUARE_CLOSE) break;
						}else{
							parser.unexpected(next, 'SQUARE_CLOSE or COMMA');
						}
					}
				}
				
				parser.assertToken(1, [tokensType.SQUARE_CLOSE], true);
				parser.skip(1);
				
				return createPToken(tk, function(){
					return '[' + arrValues.map(function(v){return v.compileJS();}).join(',') + ']';
				});
			
			}else if(tk.type == tokensType.CURLY_OPEN){
				var objValues = [];
				
				var next = parser.peek(1);
				
				if(next.type != tokensType.CURLY_CLOSE){
					while(true){
						next = parser.nextToken();
						
						var name;
						if(next.type == tokensType.IDENTIFIER || next.type == tokensType.STRING){
							name = next.value;
						}else{
							parser.unexpected(next, 'IDENTIFIER or STRING');
						}
						
						parser.assertToken(1, [tokensType.COLON], true);
						parser.skip(1);
						
						var value = expression();
						
						objValues.push({name:name, value:value});
						
						next = parser.peek(1);
						if(next.type == tokensType.CURLY_CLOSE){
							break;
						}else if(next.type == tokensType.COMMA){
							parser.skip(1);
							if(parser.peek(1).type == tokensType.CURLY_CLOSE) break;
						}else{
							parser.unexpected(next, 'CURLY_CLOSE or COMMA');
						}
					}
				}
				
				parser.assertToken(1, [tokensType.CURLY_CLOSE], true);
				parser.skip(1);
				
				return createPToken(tk, function(){
					return '({'+objValues.map(function(v){return v.name+':'+v.value.compileJS()}).join(',')+'})';
				});
			}else if(tk.type == tokensType.REGEX){
				// Test regex
				try{
					var testRegex = new RegExp(tk.value);
				}catch(e){
					throw new Error("Invalid regex at line " + tk.line);
				}
				
				return createPToken(tk, function(){
					return '(' + tk.value + ')';
				});
			}else if(tk.type == tokensType.FUNCTION){
				return createFunction();
			}else if(tk.type == tokensType.DELAY){
				var tmpTk = parser.peek(1);
				var exp;
				var body;
				
				if(tmpTk.type == tokensType.ROUND_OPEN){
					parser.assertToken(1, [tokensType.ROUND_OPEN], true);
					parser.skip(1);
					exp = expression();
					parser.assertToken(1, [tokensType.ROUND_CLOSE], true);
					parser.skip(1);
				}else{
					exp = createPToken(tk, function(){return '1';});
				}
				
				body = statement();
				
				return createPToken(tk, function(){
					return 'setTimeout(function(){'+body.compileJS()+'}, '+exp.compileJS()+');';
				});
			}else if(tk.type == tokensType.NULL){
				return createPToken(tk, function(){return 'null';});
			}else if(tk.type == tokensType.TRUE){
				return createPToken(tk, function(){return 'true';});
			}else if(tk.type == tokensType.FALSE){
				return createPToken(tk, function(){return 'false';});
			}else if(tk.type == tokensType.UNDEFINED){
				return createPToken(tk, function(){return '(void 0)';});
			}
			
			parser.unexpected(tk);
			return null;
		}
		
		function memberExpression(){
			/** @const */
			var higherPrecedence = primaryExpression;
			
			if(parser.peek(1).type == tokensType.NEW){
				var tk = parser.nextToken();
				var cls = higherPrecedence();
				
				var roundOpen = parser.nextToken();
				if(roundOpen.type != tokensType.ROUND_OPEN) parser.unexpected(roundOpen, 'ROUND_OPEN');
				
				var args = getFunctionCallArgs();
				
				var roundClose = parser.nextToken();
				if(roundClose.type != tokensType.ROUND_CLOSE) parser.unexpected(roundClose, 'ROUND_CLOSE');
				
				return createPToken(tk, function(){
					return '(new ('+this.cls.compileJS()+')('+this.args.map(function(v){return v.compileJS()}).join(',')+'))';
				},
				{
					cls: cls,
					args: args
				});
				
			}else{
				var left = higherPrecedence();
				var tk = parser.nextToken();
				while(tk.type == tokensType.DOT || tk.type == tokensType.SQUARE_OPEN){
					var ptk;
					if(tk.type == tokensType.DOT){
						var right = parser.nextToken();
						if(right.type != tokensType.IDENTIFIER) parser.unexpected(right, 'IDENTIFIER');
						
						
						ptk = createPToken(tk, function(){
							return '('+this.left.compileJS() + '.' + this.right.value+')';
						}, {
							left: left,
							right: right
						});
					}else{
						var right = expression();
						
						var squareClose = parser.nextToken();
						if(squareClose.type != tokensType.SQUARE_CLOSE) parser.unexpected(squareClose, 'SQUARE_CLOSE');
						
						ptk = createPToken(tk, function(){
							return '('+this.left.compileJS() + '[' + this.right.compileJS()+'])';
						}, {
							left: left,
							right: right
						});
					}
					
					
					left = ptk;
					tk = parser.nextToken();
				}
				
				parser.back(1);
				
				return left;
			}
		}
		
		function callExpression(){
			/** @const */
			var higherPrecedence = memberExpression;
			
			var left = higherPrecedence();
			var tk;
			while((tk = parser.peek(1)).type == tokensType.ROUND_OPEN){
				parser.skip(1);
				var args = getFunctionCallArgs();
				
				var roundClose = parser.nextToken();
				if(roundClose.type != tokensType.ROUND_CLOSE) parser.unexpected(roundClose, 'ROUND_CLOSE');
				
				var ptk = createPToken(tk, function(){
					return '('+this.left.compileJS()+'('+this.args.map(function(v){return v.compileJS()}).join(',')+'))';
				},
				{
					left: left,
					args: args
				});
				
				left = ptk;
			}
			
			return left;
		}
		
		function incrementExpression(){
			/** @const */
			var higherPrecedence = callExpression;
			
			var tk = parser.peek(1);
			var tk2 = parser.peek(2);
			if((tk.type == tokensType.INCREMENT || tk.type == tokensType.DECREMENT)){
				var op = tk.type == tokensType.INCREMENT ? '++' : '--';
				
				tk = parser.nextToken();
				
				var right = parser.nextToken();
				if(right.type != tokensType.IDENTIFIER) parser.unexpected(right, 'IDENTIFIER');
				return createPToken(tk, function(){
					return '('+op+right.value+')';
				});
			}else if(tk.type == tokensType.IDENTIFIER && (tk2.type == tokensType.INCREMENT || tk2.type == tokensType.DECREMENT)){
				var op = tk2.type == tokensType.INCREMENT ? '++' : '--';
				
				tk = parser.nextToken();
				tk2 = parser.nextToken();
				
				if(tk.type != tokensType.IDENTIFIER) parser.unexpected(tk, 'IDENTIFIER');
				return createPToken(tk, function(){
					return '('+tk.value+op+')';
				});
			}
			
			return higherPrecedence();
		}
		
		function getFunctionCallArgs(){
			if(parser.peek(1).type == tokensType.ROUND_CLOSE) return [];
			var args = [];
			
			args.push(expression());
			while(true){
				var tk = parser.nextToken();
				if(tk.type == tokensType.COMMA){
					args.push(expression());
				}else{
					parser.back(1);
					break;
				}
			}
			
			
			return args;
		}
		
		var multExpression = generateLeftOperatorParser(incrementExpression, ['*', '/', '%']);
		var addExpression = generateLeftOperatorParser(multExpression, ['+', '-']);
		var bitwiseShiftExpression = generateLeftOperatorParser(addExpression, ['<<', '>>', '>>>']);
		var relationalExpression = generateLeftOperatorParser(bitwiseShiftExpression, ['<', '>', '<=', '>=']);
		var equalityExpression = generateLeftOperatorParser(relationalExpression, ['==', '===', '!=', '!==']);
		var bitwiseAndExpression = generateLeftOperatorParser(equalityExpression, ['&']);
		var bitwiseXorExpression = generateLeftOperatorParser(bitwiseAndExpression, ['^']);
		var bitwiseOrExpression = generateLeftOperatorParser(bitwiseXorExpression, ['|']);
		var bitwiseLogicalAndExpression = generateLeftOperatorParser(bitwiseOrExpression, ['&&']);
		var bitwiseLogicalOrExpression = generateLeftOperatorParser(bitwiseLogicalAndExpression, ['||']);
		
		function isExpression(){
			/** @const */
			var higherPrecedence = bitwiseLogicalOrExpression;
			
			var left = higherPrecedence();
			var tk = parser.nextToken();
			while(tk.type == tokensType.IS){
				var ptk = createPToken(tk, function(){
					return '('+this.left.compileJS() + ' instanceof ' + this.right.compileJS()+')';
				}, {
					left: left,
					right: higherPrecedence()
				});
				
				left = ptk;
				tk = parser.nextToken();
			}
			
			parser.back(1);
			
			return left;
		}
		
		function conditionalExpression(){
			/** @const */
			var higherPrecedence = isExpression;
			
			var left = higherPrecedence();
			var tk = parser.nextToken();
			while(tk.type == tokensType.QUESTIONMARK){
				var ifTrue = higherPrecedence();
				parser.assertToken(1, [tokensType.COLON], true);
				parser.skip(1);
				var ifFalse = higherPrecedence();
				
				var ptk = createPToken(tk, function(){
					return '('+this.cond.compileJS() + '?' + this.ifTrue.compileJS() + ':' + this.ifFalse.compileJS() + ')';
				}, {
					cond: left,
					ifTrue: ifTrue,
					ifFalse: ifFalse
				});
				
				left = ptk;
				tk = parser.nextToken();
			}
			
			parser.back(1);
			
			return left;
		}
		
		function assignExpression(){
			/** @const */
			var higherPrecedence = conditionalExpression;
			
			var left = higherPrecedence();
			
			var tk = parser.peek(1);
			if(tk.type == tokensType.ASSIGN){
				if(!left.lefthand){
					throw new Error("Expression on the left must be assignable at line " + tk.line);
				}
				
				tk = parser.nextToken();
				var exp = assignExpression();
				return createPToken(tk, function(){
					return '('+left.compileJS() + tk.value + exp.compileJS()+')';
				});
			}
			
			return left;
		}
		
		function commaExpression(){
			/** @const */
			var higherPrecedence = assignExpression;
			
			var left = higherPrecedence();
			var tk = parser.nextToken();
			while(tk.type == tokensType.COMMA){
				var ptk = createPToken(tk, function(){
					return '('+this.left.compileJS() + ',' + this.right.compileJS()+')';
				}, {
					left: left,
					right: higherPrecedence()
				});
				
				left = ptk;
				tk = parser.nextToken();
			}
			
			parser.back(1);
			
			return left;
		}
		
		/**
		 * Parses an expression
		 * @return {PToken}
		 */
		function expression(){
			return assignExpression();
		}
		
		function getLeftHandAssignment(){
			/**
			 * @enum {number}
			 * @const
			 */
			var accessType = {IDENTIFIER:0, ARRAY:1};
			var access = [];
			
			
			function pushAccess(type, value){
				access.push({type:type,value:value});
			}
			
			var firstToken = parser.nextToken();
			switch(firstToken.type){
				case tokensType.IDENTIFIER:
					pushAccess(accessType.IDENTIFIER, firstToken.value);
				break;
				
				case tokensType.AT:
					pushAccess(accessType.IDENTIFIER, 'this');
				break;
				
				default: parser.unexpected(firstToken);
			}
			
			var firstLoop = true;
			
			while(true){
				var tk = parser.peek(1);
				if(firstLoop && firstToken.type == tokensType.AT && tk.type == tokensType.IDENTIFIER){
					tk = parser.nextToken(); // IDENTIFIER
					pushAccess(accessType.IDENTIFIER, tk.value);
				}else if(tk.type == tokensType.DOT){
					tk = parser.nextToken(); // DOT
					parser.assertToken(1, [tokensType.IDENTIFIER], true);
					tk = parser.nextToken(); // IDENTIFIER
					pushAccess(accessType.IDENTIFIER, tk.value);
				}else if(tk.type == tokensType.SQUARE_OPEN){
					tk = parser.nextToken(); // SQUARE_OPEN
					var exp = expression();
					parser.assertToken(1, [tokensType.SQUARE_CLOSE], true);
					tk = parser.nextToken(); // SQUARE_CLOSE
					pushAccess(accessType.ARRAY, exp);
				}else if(tk.type == tokensType.DCOLON){
					parser.skip(1);
					parser.assertToken(1, [tokensType.IDENTIFIER], true);
					tk = parser.nextToken(); // IDENTIFIER
					pushAccess(accessType.IDENTIFIER, 'prototype');
					pushAccess(accessType.IDENTIFIER, tk.value);
				}else{
					break;
				}
				firstLoop = false;
			}
			
			
			return createPToken(firstToken, function(){
				var js = access[0].value; // Type guaranteed to be IDENTIFIER, so value is a string
				for(var i=1;i<access.length;++i){
					var ac = access[i];
					switch(ac.type){
						case accessType.IDENTIFIER:
							js += '.'+ac.value;
						break;
						case accessType.ARRAY:
							js += '['+ac.value.compileJS()+']';
						break;
					}
				}
				return js;
			}, {lefthand:true});
		}
		
		
		
		/**
		 * Creates a function PToken, the FUNCTION token must be at position 0
		 * It's also possible that a IDENTIFIER token with value "constructor" be at position 0
		 * @return {PToken}
		 */
		
		function createFunction(){
			var tk = parser.peek(0);
			var isConstructor = tk.type == tokensType.IDENTIFIER && tk.value == 'constructor';
			var next = parser.nextToken();
			var funcName;
			var protoFunc = false;
			
			switch(next.type){
				case tokensType.IDENTIFIER:
					funcName = next.value;
					var next = parser.nextToken();
					if(next.type == tokensType.ROUND_OPEN){
						// Ignore
					}else if(next.type == tokensType.DCOLON){
						if(isConstructor) throw new Error("Prototype function cannot be constructor at line "+tk.line);
						protoFunc = true;
						parser.assertToken(1, [tokensType.IDENTIFIER], true);
						
						funcName += '.prototype.'+parser.nextToken().value;
						
						parser.assertToken(1, [tokensType.ROUND_OPEN], true);
						parser.skip(1);
					}else{
						parser.unexpected(next, 'ROUND_OPEN or DCOLON');
					}
				break;
				
				case tokensType.ROUND_OPEN:
					funcName = null;
				break;
				
				default: parser.unexpected(next);
			}
			
			var header = functionHeader();
			
			parser.assertToken(1, [tokensType.CURLY_OPEN, tokensType.ARROW], true);
			parser.skip(1);
			
			if(parser.peek(0).type == tokensType.CURLY_OPEN){
				var body = statementList();
				
				parser.assertToken(1, [tokensType.CURLY_CLOSE], true);
				parser.skip(1);
				
				if(protoFunc){
					return createPToken(tk, function(){
						return funcName + '=function('+header.join(',')+'){' + body.compileJS() + '}';
					});
				}else{
					return createPToken(tk, function(){
						return (isConstructor?'/** @constructor */':'')+'function' + (funcName ? ' '+funcName:'') + '('+header.join(',')+'){' + body.compileJS() + '}';
					});
				}
			}else{
				var exp = expression();
				
				if(protoFunc){
					return createPToken(tk, function(){
						return funcName + '=function('+header.join(',')+'){return ' + exp.compileJS() + '}';
					});
				}else{
					return createPToken(tk, function(){
						return (isConstructor?'/** @constructor */':'')+'function' + (funcName ? ' '+funcName:'') + '('+header.join(',')+'){return ' + exp.compileJS() + '}';
					});
				}
			}
		}
		
		/**
		 * Creates a var PToken, the VAR token must be at position 0
		 * @return {PToken}
		 */
		function createVar(){
			var tk = parser.peek(0);
			var isConst = tk.type == tokensType.CONST;
			
			var vars = [];
			
			while(true){
				parser.assertToken(1, [tokensType.IDENTIFIER], true);
				var nameTk = parser.nextToken();
				var name = nameTk.value;
				
				var next = parser.nextToken();
				if(next.type == tokensType.SEMICOLON){
					vars.push(createPToken(nameTk, function(){return this.name;}, {name:name}));
					break;
				}else if(next.type == tokensType.ASSIGN && next.value == '='){
					var exp = expression();
					vars.push(createPToken(nameTk, function(){return this.name+'='+this.value.compileJS()}, {name:name, value:exp}));
					
					next = parser.nextToken();
					if(next.type == tokensType.SEMICOLON){
						break;
					}else if(next.type == tokensType.COMMA){
						continue;
					}else{
						parser.unexpected(next, 'SEMICOLON or COMMA');
					}
					
				}else if(next.type == tokensType.COMMA){
					vars.push(createPToken(nameTk, function(){return this.name;}, {name:name}));
					continue;
				}else{
					parser.unexpected(next, 'SEMICOLON, ASSIGN(=) or COMMA');
				}
			}
			
			return createPToken(tk, function(){
				var str = isConst ? '/** @const */ var ' : 'var ';
				for(var i=0;i<vars.length;++i){
					if(i != 0) str+=',';
					str += vars[i].compileJS();
				}
				return str + ';';
			});
		}
		
		/**
		 * Creates a switch PToken, the SWITCH token must be at position 0
		 * @return {PToken}
		 */
		function createSwitch(){
			var firstToken = parser.peek(0);
			
			parser.assertToken(1, [tokensType.ROUND_OPEN], true);
			parser.skip(1);
			
			var switchExp = expression();
			
			parser.assertToken(1, [tokensType.ROUND_CLOSE], true);
			parser.skip(1);
			
			parser.assertToken(1, [tokensType.CURLY_OPEN], true);
			parser.skip(1);
			
			var defaultCase = null;
			var cases = [];
			
			while(true){
				var tk = parser.peek(1);
				if(tk.type == tokensType.CURLY_CLOSE){
					break;
				}else if(tk.type == tokensType.DEFAULT){
					parser.skip(1);
					parser.assertToken(1, [tokensType.COLON], true);
					parser.skip(1);
					
					defaultCase = statementList();
					break;
				}else if(tk.type == tokensType.CASE){
					parser.skip(1);
					
					var caseArgs = [];
					caseArgs.push(expression());
					
					parser.assertToken(1, [tokensType.COLON], true);
					parser.skip(1);
					
					while(parser.peek(1).type == tokensType.CASE){
						parser.skip(1);
						caseArgs.push(expression());
						parser.assertToken(1, [tokensType.COLON], true);
						parser.skip(1);
					}
					
					
					var body = statementList();
					cases.push({conditions:caseArgs, body:body});
				}else{
					parser.unexpected(tk, 'CURLY_CLOSE, CASE or DEFAULT');
				}
			}
			
			parser.assertToken(1, [tokensType.CURLY_CLOSE], true);
			parser.skip(1);
			
			return createPToken(firstToken, function(){
				var str = 'switch('+switchExp.compileJS()+'){';
				
				for(var i=0;i<cases.length;++i){
					for(var j=0;j<cases[i].conditions.length;++j){
						str += 'case '+cases[i].conditions[j].compileJS()+':';
					}
					
					str += '{'+cases[i].body.compileJS()+'}break;';
				}
				
				if(defaultCase) str += 'default:'+defaultCase.compileJS();
				str += '}';
				return str;
			});
		}
		
		/**
		 * Creates a if PToken, the IF token must be at position 0
		 * @return {PToken}
		 */
		function createIf(){
			var ifTk = parser.peek(0);
			
			parser.assertToken(1, [tokensType.ROUND_OPEN], true);
			parser.skip(1);
			
			var ifCond = expression();
			
			parser.assertToken(1, [tokensType.ROUND_CLOSE], true);
			parser.skip(1);
			
			var ifBody = statement();
			
			if(parser.peek(1).type == tokensType.ELSE){
				parser.skip(1);
				var elseBody = statement();
				return createPToken(ifTk, function(){
					return 'if('+ifCond.compileJS()+'){'+ifBody.compileJS()+'}else{'+elseBody.compileJS()+'}';
				});
			}
			
			return createPToken(ifTk, function(){
				return 'if('+ifCond.compileJS()+'){'+ifBody.compileJS()+'}';
			});
		}
		
		/**
		 * Parses the tokens of a function header, the ROUND_OPEN should be at the position 0
		 * This will consume the ROUND_CLOSE, and it'll be at the position 0 after this function returns
		 * 
		 * @return {Array.<string>}
		 */
		function functionHeader(){
			var args = [];
			var cur = parser.peek(1);
			if(cur.type == tokensType.ROUND_CLOSE){
				parser.skip(1);
				return args;
			}
			
			var firstLoop = true;
			
			while(true){
				cur = parser.nextToken();
				if(cur.type != tokensType.IDENTIFIER){
					if(firstLoop){
						parser.unexpected(cur, 'IDENTIFIER or ROUND_CLOSE');
					}else{
						parser.unexpected(cur, 'IDENTIFIER');
					}
				}
				args.push(cur.value);
				
				firstLoop = false;
				
				cur = parser.nextToken();
				if(cur.type == tokensType.COMMA){
					continue;
				}else if(cur.type == tokensType.ROUND_CLOSE){
					break;
				}else{
					parser.unexpected(cur, 'COMMA or ROUND_CLOSE');
				}
			}
			
			return args;
		}
		
		/**
		 * Parses a statement
		 * 
		 * @return {PToken}
		 */
		function statement(){
			/** @type {Token} */
			var tk = parser.peek(1);
			
			if(tk.type == tokensType.IDENTIFIER && parser.peek(2).type == tokensType.COLON){
				parser.skip(2);
				return createPToken(tk, function(){return tk.value+':'});
			}
			
			if(tk.type == tokensType.IDENTIFIER && tk.value == "constructor" && parser.peek(2).type == tokensType.IDENTIFIER){
				parser.skip(1);
				return createFunction();
			}
			
			switch(tk.type){
				case tokensType.STRING:
				case tokensType.NUMBER:
				case tokensType.ROUND_OPEN:
				case tokensType.SQUARE_OPEN:
				case tokensType.NEW:
				case tokensType.OPERATOR:
				case tokensType.IDENTIFIER:
				case tokensType.INCREMENT:
				case tokensType.DECREMENT:
				case tokensType.DELAY:
				case tokensType.AT:
				case tokensType.NULL:
				case tokensType.TRUE:
				case tokensType.FALSE:
				case tokensType.UNDEFINED:
				case tokensType.REGEX:
					var exp = expression();
					
					if(parser.peek(0).type != tokensType.CURLY_CLOSE){
						parser.assertToken(1, [tokensType.SEMICOLON], true);
						parser.skip(1);
					}
					
					return createPToken(tk, function(){
						return exp.compileJS()+';';
					});
				break;
				
				case tokensType.IF:
					parser.skip(1);
					return createIf();
				break;
				case tokensType.WHILE:
					parser.skip(1);
					parser.assertToken(1, [tokensType.ROUND_OPEN], true);
					parser.skip(1);
					var exp = expression();
					parser.assertToken(1, [tokensType.ROUND_CLOSE], true);
					parser.skip(1);
					var stat = statement();
					return createPToken(tk, function(){
						return 'while('+exp.compileJS()+'){'+stat.compileJS()+'}';
					});
				break;
				
				case tokensType.RETURN:
					parser.skip(1);
					
					if(parser.peek(1).type != tokensType.SEMICOLON){
						var exp = expression();
						parser.assertToken(1, [tokensType.SEMICOLON], true);
						parser.skip(1);
						return createPToken(tk, function(){
							return 'return '+exp.compileJS()+';';
						});
					}else{
						parser.skip(1);
						return createPToken(tk, function(){
							return 'return;';
						});
					}
				break;
				
				case tokensType.CONTINUE:
				case tokensType.BREAK:
					var type = tk.type == tokensType.CONTINUE ? 'continue' : 'break';
					parser.skip(1);
					
					if(parser.peek(1).type != tokensType.SEMICOLON){
						parser.assertToken(1, [tokensType.IDENTIFIER], true);
						var labelName = parser.nextToken().value;
						parser.assertToken(1, [tokensType.SEMICOLON], true);
						parser.skip(1);
						return createPToken(tk, function(){
							return type+' '+labelName+';';
						});
					}else{
						parser.skip(1);
						return createPToken(tk, function(){
							return type+';';
						});
					}
				break;
				
				case tokensType.VAR:
				case tokensType.CONST:
					parser.skip(1);
					return createVar();
				break;
				
				case tokensType.FUNCTION:
					parser.skip(1);
					return createFunction();
				break;
				
				case tokensType.CURLY_OPEN:
					parser.skip(1);
					var body = statementList();
					parser.assertToken(1, [tokensType.CURLY_CLOSE], true);
					parser.skip(1);
					return createPToken(tk, function(){return '{'+body.compileJS()+'}';});
				break;
				
				case tokensType.SWITCH:
					parser.skip(1);
					return createSwitch();
				break;
				
				case tokensType.THROW:
					parser.skip(1);
					
					if(parser.peek(1).type != tokensType.SEMICOLON){
						var exp = expression();
						parser.assertToken(1, [tokensType.SEMICOLON], true);
						parser.skip(1);
						return createPToken(tk, function(){
							return 'throw '+exp.compileJS()+';';
						});
					}else{
						parser.skip(1);
						return createPToken(tk, function(){
							return 'throw;';
						});
					}
				break;
				
				case tokensType.FOR:
					parser.skip(1);
					
					var next = parser.nextToken();
					
					if(next.type == tokensType.ROUND_OPEN){
						next = parser.peek(1);
						if(next.type == tokensType.IDENTIFIER && parser.peek(2).type == tokensType.IN){
							parser.skip(2);
							
							var start = expression();
							
							parser.assertToken(1, [tokensType.ELLIPSIS], true);
							parser.skip(1);
							
							var end = expression();
							
							parser.assertToken(1, [tokensType.ROUND_CLOSE], true);
							parser.skip(1);
							
							var body = statement();
							
							var lenVarName = getTmpVarName();
							
							return createPToken(tk, function(){
								return 'for(var '+next.value+'='+start.compileJS()+',/** @const */'+lenVarName+'='+end.compileJS()+';'+next.value+'<'+lenVarName+';++'+next.value+'){'+body.compileJS()+'};';
							});
							
						}else{
							// This part is a string, the others are PToken
							var firstPart;
							
							if(next.type == tokensType.VAR){
								firstPart = createVar().compileJS();
							}else{
								firstPart = expression().compileJS()+';';
								parser.assertToken(1, [tokensType.SEMICOLON], true);
								parser.skip(1);
							}
							
							var secondPart = expression();
							parser.assertToken(1, [tokensType.SEMICOLON], true);
							parser.skip(1);
							
							var thirdPart = expression();
							
							parser.assertToken(1, [tokensType.ROUND_CLOSE], true);
							parser.skip(1);
							
							var body = statement();
							
							return createPToken(tk, function(){
								return 'for('+firstPart+secondPart.compileJS()+';'+thirdPart.compileJS()+'){'+body.compileJS()+'}';
							});
						}
					}else if(next.type == tokensType.EACH){
						parser.assertToken(1, [tokensType.ROUND_OPEN], true);
						parser.skip(1);
						
						if(parser.peek(1).type == tokensType.VAR) parser.skip(1);
						
						parser.assertToken(1, [tokensType.IDENTIFIER], true);
						var varName = parser.nextToken().value;
						
						parser.assertToken(1, [tokensType.IN], true);
						parser.skip(1);
						
						var iteratorExp = expression();
						
						parser.assertToken(1, [tokensType.ROUND_CLOSE], true);
						parser.skip(1);
						
						var body = statement();
						
						return createPToken(tk, function(){
							var iteratorName = getTmpVarName();
							return "for(var " + iteratorName + "=" + iteratorExp.compileJS() + ".iterator();" + iteratorName + ".hasNext();){var " + varName + "=" + iteratorName + ".next();" + body.compileJS() + "}";
						});
					}else{
						parser.unexpected(next, 'ROUND_OPEN or EACH');
					}
				break;
				
				case tokensType.SCOPE:
					parser.skip(1);
					
					switch(parser.peek(1).type){
						case tokensType.CURLY_OPEN:
							parser.skip(1);
							
							var body = statementList();
							
							parser.assertToken(1, [tokensTpe.CURLY_CLOSE], true);
							parser.skip(1);
							
							return createPToken(tk, function(){
								return '(function(){'+body.compileJS()+'})();';
							});
						break;
						case tokensType.ROUND_OPEN:
							parser.skip(1);
							var header = functionHeader();
							var body = statement();
							return createPToken(tk, function(){
								return '(function('+header.join(',')+'){'+body.compileJS()+'})('+header.join(',')+');';
							});
						break;
						default:
							parser.unexpected(parser.peek(1), 'ROUND_OPEN or CURLY_OPEN');
						break;
					}
					
				break;
				
				default:
					throw new Error("Expected statement, got "+getTokenName(tk));
				break;
			}
		}
		
		/**
		 * Parses a statement list
		 * 
		 * @param {boolean=} fatal
		 * @return {PToken}
		 */
		function statementList(fatal){
			var inLoop = true;
			/** @type {Array.<PToken>} */
			var stats = [];
			var firstToken = parser.peek(1);
			while(inLoop){
				var tk = parser.peek(1);
				switch(tk.type){
					case tokensType.STRING:
					case tokensType.NUMBER:
					case tokensType.ROUND_OPEN:
					case tokensType.SQUARE_OPEN:
					case tokensType.NEW:
					case tokensType.OPERATOR:
					case tokensType.IDENTIFIER:
					case tokensType.INCREMENT:
					case tokensType.DECREMENT:
					case tokensType.FUNCTION:
					case tokensType.VAR:
					case tokensType.RETURN:
					case tokensType.FOR:
					case tokensType.WHILE:
					case tokensType.IF:
					case tokensType.DO:
					case tokensType.DELAY:
					case tokensType.CURLY_OPEN:
					case tokensType.AT:
					case tokensType.SWITCH:
					case tokensType.NULL:
					case tokensType.TRUE:
					case tokensType.FALSE:
					case tokensType.UNDEFINED:
					case tokensType.CONST:
					case tokensType.THROW:
					case tokensType.REGEX:
					case tokensType.BREAK:
					case tokensType.CONTINUE:
					case tokensType.SCOPE:
						stats.push(statement());
					break;
					
					case tokensType.EOF:
						inLoop = false;
					break;
					
					case tokensType.SEMICOLON:
						parser.skip(1);
						stats.push(createPToken(tk, function(){return ';';}));
					break;
					
					case tokensType.CURLY_CLOSE:
						inLoop = false;
					break;
					
					default:
						if(fatal || stats.length == 0){
							parser.unexpected(tk);
						}else{
							inLoop = false;
						}
					break;
				}
			}
			
			return createPToken(firstToken, function(){
				return stats.map(function(v){return v.compileJS();}).join('');
			});
		}
		
		return statementList(true);
	}
	
	
	var jse = {};
	jse['tokenize'] = tokenize;
	jse['compile'] = compile;
	
	global['$jse'] = jse;
	
	
	
	
	
	function makeAjaxRequest(url, callback, method){
		var XMLHttpArray = [
			function() {return new XMLHttpRequest()},
			function() {return new ActiveXObject("Msxml2.XMLHTTP")},
			function() {return new ActiveXObject("Msxml2.XMLHTTP")},
			function() {return new ActiveXObject("Microsoft.XMLHTTP")}
		];
		
		var req = null;
		for(var i=0; i < XMLHttpArray.length; i++){
			try{
				req = XMLHttpArray[i]();
				break;
			}catch(e){}
		}
		
		if(req == null) throw new Error("Couldn't make ajax request");
		
		req.onreadystatechange= function(){
			if(req.readyState != 4) return;
			if(req.status != 200) return;
			callback(req);
		};
		req.open(method,url,true);
		req.send(null);
	}
	
	
	
	if(typeof window != 'undefined'){
		var cacheKey = Math.random();
		var autoInterpret = function(e){
			var scripts = document.getElementsByTagName('script');
			var len = scripts.length;
			
			var codes = [];
			var curId = 0;
			var loading = 0;
			
			var checkLoading = function(){
				if(loading > 0) return;
				for(var i=0;i<codes.length;++i){
					if(!codes[i]) continue;
					evalScript(codes[i]);
				}
			}
			
			for(var i=0;i<len;++i){
				var script = scripts[i];
				if(script.type != 'text/jse') continue;
				if(script['$jseCache'] == cacheKey) continue;
				
				if(script.src){
					++loading;
					(function(id){
						makeAjaxRequest(script.src, function(req){
							var scriptCode = req.responseText;
							codes[id] = compile(scriptCode);
							--loading;
							checkLoading();
						}, 'GET');
					})(curId++);
				}else if(script.innerText){
					codes[curId++] = compile(script.innerText);
				}
				
				script['$jseCache'] = cacheKey;
			}
			
			
			
			checkLoading();
		}
		
		var evalScript = function(code){
			var scriptElem = document.createElement("script");
			scriptElem.innerText = code;
			document.body.appendChild(scriptElem);
		}
		
		if(window.addEventListener){
			window.addEventListener('load', autoInterpret, false); 
		}else if(window.attachEvent){
			window.attachEvent('onload', autoInterpret);
		}else{
			var oldLoad = window.onload;
			window.onload = function(e){
				oldLoad(e);
				autoInterpret(e);
			}
		}
	}
})(typeof window != 'undefined' ? window : exports);
